﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Hydra.Shell.Interfaces;
using System.Reflection;
using Hydra.Shell.Attributes;
using System.Threading.Tasks;
using SLControlLibrary;
using System.Windows.Controls;
using System.Diagnostics;
using Hydra.Shell.Enumerations;
using Hydra.Shell.Exceptions;
using UIObjects;
using System.Windows;
using System.Windows.Threading;
using Utils;

namespace Hydra.Shell.Implementations
{
    [Guid("9D43D521-1758-43D8-80A1-57105485093C")]
    public abstract class HydraShell : HydraServiceBase<HydraShell>, IHydraShell, IHydraUIInternals, IHydraProfferService
    {
        public event OnRoutedEventHandler OnRoutedEvent;
        public event OnShellPropertyChangeHandler OnShellPropertyChange;
        protected abstract Dictionary<Guid, Assembly> PackageAssemblies { get; }
        protected abstract Dictionary<Guid, Assembly> ProtocolHandlerAssemblies { get; }
        public abstract ILog Log { get; set; }
        public abstract Dictionary<Guid, HydraPackageWrapper> LoadedPackages { get; }
        public abstract Dictionary<Guid, HydraServiceWrapper> LoadedServices { get; }
        public abstract Dictionary<Guid, IHydraPackageListItem> PackageList { get; }
        public abstract Dictionary<Guid, IProtocolHandlerListItem> ProtocolHandlersList { get; }
        public abstract Dictionary<Guid, IHydraEditorListItem> EditorsList { get; }
        public abstract HandlerLoadOperation LoadProtocolHandler(Guid guidHandler);
        public abstract ItemGenerationOperation GenerateAbstraXItemFromID(string id);
        protected abstract Dictionary<Guid, HydraServiceListItem> ServiceList { get; }
        protected abstract ServiceLoadOperation<T> LoadService<T>(Guid guidService) where T : IUnknown;
        protected abstract ServiceLoadOperation LoadService(Guid guidService, Guid guidInterFace);
        public abstract UserControl MainPage { get; }
        public abstract PackageLoadOperation LoadPackage(Guid guidPackage);
        protected IHydraSynchronizerService synchronizationService;
        protected IPackageAgent packageAgent;
        private IHydraUIInternals hydraUIInternals;
        private uint profferServiceCookie;
        private uint profferUIInternalsCookie;
        private uint profferHydraShellCookie;
        private uint profferHydraStatusbarCookie;
        private DateTime lastEvent;
        private DispatcherTimer idleTimer;

        public HydraShell(UserControl page, IHydraUIInternals hydraUIInternals, IPackageAgent packageAgent) : base(new UIThread(page.Dispatcher))
        {
            uiThread = new UIThread(page.Dispatcher);
            this.packageAgent = packageAgent;
            this.hydraUIInternals = hydraUIInternals;

            HydraPackageWrapper.ObjectDisposed += new EventHandler(ObjectDisposed);

            this.ProfferService(typeof(IHydraShell).GetGuid(), this, out profferHydraShellCookie);
            this.ProfferService(typeof(IHydraProfferService).GetGuid(), this, out profferServiceCookie);
            this.ProfferService(typeof(IHydraUIInternals).GetGuid(), this, out profferUIInternalsCookie);
            this.ProfferService(typeof(IHydraStatusBar).GetGuid(), (IQueryServiceProvider)hydraUIInternals.StatusBar, out profferHydraStatusbarCookie);

            Application.Current.RootVisual.KeyDown += new System.Windows.Input.KeyEventHandler(KeyEvent); 
            Application.Current.RootVisual.KeyUp += new System.Windows.Input.KeyEventHandler(KeyEvent);
            Application.Current.RootVisual.MouseLeftButtonDown += new System.Windows.Input.MouseButtonEventHandler(MouseButtonEvent);
            Application.Current.RootVisual.MouseLeftButtonUp += new System.Windows.Input.MouseButtonEventHandler(MouseButtonEvent);
            Application.Current.RootVisual.MouseRightButtonDown += new System.Windows.Input.MouseButtonEventHandler(MouseButtonEvent);
            Application.Current.RootVisual.MouseRightButtonUp += new System.Windows.Input.MouseButtonEventHandler(MouseButtonEvent);
            Application.Current.RootVisual.MouseWheel += new System.Windows.Input.MouseWheelEventHandler(MouseWheelEvent);
            Application.Current.RootVisual.TextInput += new System.Windows.Input.TextCompositionEventHandler(TextCompositionEvent);
            Application.Current.RootVisual.TextInputStart += new System.Windows.Input.TextCompositionEventHandler(TextCompositionEvent);
            Application.Current.RootVisual.TextInputUpdate += new System.Windows.Input.TextCompositionEventHandler(TextCompositionEvent);

            lastEvent = DateTime.Now;

            idleTimer = new DispatcherTimer();

            idleTimer.Interval = TimeSpan.FromSeconds(1);

            idleTimer.Tick += new EventHandler(IdleTimerTick);

            idleTimer.Start();
        }

        private void IdleTimerTick(object sender, EventArgs e)
        {
            if (DateTime.Now - lastEvent > TimeSpan.FromMilliseconds(500))
            {
                foreach (var packageWrapper in this.LoadedPackages.Values)
                {
                    if (packageWrapper.PackageType.Implements<IHydraComponent>())
                    {
                        IHydraComponent component;

                        packageWrapper.Package.QueryInterface<IHydraComponent>(out component);

                        uiThread.Run(() =>
                        {
                            component.DoIdle(HydraIdle.Priority);
                        });
                    }
                }
            }
        }

        private void TextCompositionEvent(object sender, System.Windows.Input.TextCompositionEventArgs e)
        {
            lastEvent = DateTime.Now;
        }

        private void MouseWheelEvent(object sender, System.Windows.Input.MouseWheelEventArgs e)
        {
            lastEvent = DateTime.Now;
        }

        private void MouseButtonEvent(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            lastEvent = DateTime.Now;
        }

        private void KeyEvent(object sender, System.Windows.Input.KeyEventArgs e)
        {
            lastEvent = DateTime.Now;
        }

        public override T GetService<T>()
        {
            var guidService = typeof(T).GetGuid();
            var service = this.FindService<T>(guidService);

            if (service == null)
            {
                return base.GetService<T>();
            }


            return service;
        }

        public override bool QueryService<T>(Guid guidService, out ServiceLoadOperation<T> serviceLoadOperation)
        {
            var service = (IUnknown)null;

            try
            {
                service = this.FindService<T>(guidService);

                if (service == null)
                {
                    serviceLoadOperation = this.LoadService<T>(guidService);
                    return true;
                }
            }
            catch (ServiceAvailableButRequiresDependenciesException ex)
            {
                serviceLoadOperation = ex.Resolve<T>(uiThread);
            }
            catch (Exception ex)
            {
                var serviceLoadOperation2 = new ServiceLoadOperation<T>(uiThread);

                serviceLoadOperation2.DelaySetException(ex);

                serviceLoadOperation = serviceLoadOperation2;
                return false;
            }

            if (service != null)
            {
                var serviceLoadOperation2 = new ServiceLoadOperation<T>(uiThread);

                serviceLoadOperation2.DelaySetLoaded((IUnknown)service);

                serviceLoadOperation = serviceLoadOperation2;
                return true;
            }

            serviceLoadOperation = null;
            return false;
        }

        public override bool QueryService(Guid guidService, Guid guidInterFace, out ServiceLoadOperation serviceLoadOperation)
        {
            var objService = (object)null;

            try
            {
                objService = this.FindService(guidService, guidInterFace);

                if (objService == null)
                {
                    serviceLoadOperation = new ServiceLoadOperation(uiThread);

                    var serviceLoadOperation3 = serviceLoadOperation;

                    uiThread.Run(() =>
                    {
                        var serviceLoadOperation2 = this.LoadService(guidService, guidInterFace);

                        serviceLoadOperation2.ExceptionOccured += (sender, e) =>
                        {
                            serviceLoadOperation3.SetException(e.Exception);
                        };

                        serviceLoadOperation2.ServiceLoaded += (sender, e) =>
                        {
                            serviceLoadOperation3.SetLoaded(e.Service);
                        };

                    });

                    return true;
                }
            }
            catch (Exception ex)
            {
                var serviceLoadOperation2 = new ServiceLoadOperation(uiThread);

                serviceLoadOperation2.DelaySetException(ex);

                serviceLoadOperation = serviceLoadOperation2;
                return false;
            }

            if (objService != null)
            {
                var serviceLoadOperation2 = new ServiceLoadOperation(uiThread);

                serviceLoadOperation2.DelaySetLoaded((IUnknown)objService);

                serviceLoadOperation = serviceLoadOperation2;
                return true;
            }

            serviceLoadOperation = null;
            return false;
        }

        protected IHydraPackage FindPackage(Guid guidPackage)
        {
            if (this.LoadedPackages.ContainsKey(guidPackage))
            {
                return this.LoadedPackages[guidPackage].Package;
            }
            else if (this.PackageAssemblies.ContainsKey(guidPackage))
            {
                var assembly = this.PackageAssemblies[guidPackage];
                var packageType = assembly.GetTypes().Single(t => t.GetCustomAttributes(true).Any(a => a is GuidAttribute && ((GuidAttribute)a) == guidPackage));
                var serverSynchronizedAttribute = (ServerSynchronizedAttribute)packageType.GetCustomAttributes(true).SingleOrDefault(a => a is ServerSynchronizedAttribute);
                var package = (IHydraPackage)Activator.CreateInstance(packageType, new object[] { uiThread });

                Debug.WriteLine(package.PackageGuid);

                this.LoadedPackages.Add(guidPackage, package.Wrap());

                if (serverSynchronizedAttribute != null)
                {
                    synchronizationService.LoadServerPackage(package, serverSynchronizedAttribute.Type);
                }

                package.SetSite(this);

                return package;
            }
            else
            {
                return null;
            }
        }

        protected T FindService<T>(Guid guidService)
        {
            packageAgent.SendGeneralMessage(string.Format("Request to find service {{{0}}} with type '{1}'", guidService.ToString(), typeof(T).Name));

            if (this.ServiceList.ContainsKey(guidService))
            {
                var serviceListItem = this.ServiceList[guidService];

                if (serviceListItem.PackageGuid == Guid.Empty)
                {
                    if (this.LoadedServices.ContainsKey(guidService))
                    {
                        return (T)this.LoadedServices[guidService].Service;
                    }
                    else
                    {
                        return (T)FindService(guidService, typeof(T).GUID);
                    }
                }

                if (this.PackageAssemblies.ContainsKey(serviceListItem.PackageGuid))
                {
                    return (T) FindService(guidService, typeof(T).GUID);
                }
            }

            return default(T);
        }

        protected IUnknown FindService(Guid guidService, Guid guidInterFace)
        {
            packageAgent.SendGeneralMessage(string.Format("Request to find service {{{0}}} for interface {{{1}}}", guidService.ToString(), guidInterFace.ToString()));

            if (this.LoadedServices.ContainsKey(guidService))
            {
                var serviceListItem = this.ServiceList[guidService];
                var service = this.LoadedServices[guidService].Service;
                var interfaceObject = (IUnknown)null;

                if (service.QueryInterface(guidInterFace, out interfaceObject))
                {
                    if (this.PackageAssemblies.ContainsKey(serviceListItem.PackageGuid))
                    {
                        var packageAssembly = this.PackageAssemblies[serviceListItem.PackageGuid];

                        packageAgent.SendPackageMessage(packageAssembly, serviceListItem.PackageGuid, PackageMessageType.FoundService, service.GetType().Name + " {" + guidService.ToString() + "}");
                    }

                    return interfaceObject;
                }
            }
            else if (this.ServiceList.ContainsKey(guidService))
            {
                var serviceListItem = this.ServiceList[guidService];

                if (this.LoadedPackages.ContainsKey(serviceListItem.PackageGuid))
                {
                    var package = (IHydraPackage)this.LoadedPackages[serviceListItem.PackageGuid];
                    var packageType = package.GetType();
                    var provideServiceAttribute = (ProvideServiceAttribute)packageType.GetCustomAttributes(true).Single(a => a is ProvideServiceAttribute);
                    var serviceType = packageType.Assembly.GetTypes().Single(t => t == provideServiceAttribute.Service);
                    var serverSynchronizedAttribute = (ServerSynchronizedAttribute)serviceType.GetCustomAttributes(true).SingleOrDefault(a => a is ServerSynchronizedAttribute);
                    var service = (IUnknown)Activator.CreateInstance(serviceType, new object[] { uiThread });
                    var interfaceObject = (IUnknown)null;

                    this.LoadedServices.Add(guidService, service.Wrap(guidService));

                    if (serverSynchronizedAttribute != null)
                    {
                        synchronizationService.LoadServerService(guidService, service, serverSynchronizedAttribute.Type);
                    }

                    if (service.QueryInterface(guidInterFace, out interfaceObject))
                    {
                        packageAgent.SendPackageMessage(package, PackageMessageType.FoundService, service.GetType().Name + " {" + guidService.ToString() + "}");

                        return interfaceObject;
                    }
                }
                else if (this.PackageAssemblies.ContainsKey(serviceListItem.PackageGuid))
                {
                    var assembly = this.PackageAssemblies[serviceListItem.PackageGuid];
                    var packageType = assembly.GetTypes().Single(t => t.GetCustomAttributes(true).Any(a => a is GuidAttribute && ((GuidAttribute)a) == serviceListItem.PackageGuid));
                    var provideServiceAttributes = packageType.GetCustomAttributes(true).OfType<ProvideServiceAttribute>();
                    var serviceTypes = assembly.GetTypes().Where(t => provideServiceAttributes.Any(a => a.Service == t));
                    var serviceType = serviceTypes.Single(t => t.GetCustomAttributes(true).Any(a => a is GuidAttribute && ((GuidAttribute)a) == guidService));
                    var serverSynchronizedAttribute = (ServerSynchronizedAttribute)serviceType.GetCustomAttributes(true).SingleOrDefault(a => a is ServerSynchronizedAttribute);

                    if (serviceType.GetConstructors().Any(c => c.GetParameters().Length == 1 && c.GetParameters().First().ParameterType == typeof(UIThread)))
                    {
                        var service = (IUnknown)Activator.CreateInstance(serviceType, new object[] { uiThread });
                        var interfaceObject = (IUnknown)null;

                        this.LoadedServices.Add(guidService, service.Wrap(guidService));

                        if (serverSynchronizedAttribute != null)
                        {
                            synchronizationService.LoadServerService(guidService, service, serverSynchronizedAttribute.Type);
                        }

                        if (service.QueryInterface(guidInterFace, out interfaceObject))
                        {
                            packageAgent.SendPackageMessage(assembly, serviceListItem.PackageGuid, PackageMessageType.FoundService, service.GetType().Name + " {" + guidService.ToString() + "}");

                            return interfaceObject;
                        }
                    }
                    else if (serviceType.GetConstructors().Any(c => c.GetParameters().Length > 1 && c.GetParameters().First().ParameterType == typeof(UIThread)))
                    {
                        var constructor = serviceType.GetConstructors().Single(c => c.GetParameters().Length > 1 && c.GetParameters().First().ParameterType == typeof(UIThread));
                        var dependencyTypes = constructor.GetParameters().Where(p => p.ParameterType != typeof(UIThread)).Select(p => p.ParameterType);
                        IQueryServiceProvider queryServiceProvider;

                        if (this.QueryInterface<IQueryServiceProvider>(out queryServiceProvider))
                        {
                            var exception = new ServiceAvailableButRequiresDependenciesException(packageAgent, queryServiceProvider, dependencyTypes.ToList(), constructor);

                            packageAgent.SendPackageError(assembly, serviceListItem.PackageGuid, exception);

                            throw exception;
                        }
                        else
                        {
                            var exception = new ServiceAvailableButRequiresDependenciesException();

                            packageAgent.SendPackageError(assembly, serviceListItem.PackageGuid, exception);

                            throw exception;
                        }
                    }
                    else
                    {
                        var exception = new Exception(string.Format("Service {{{0}}} must have a constructor with the first being of type UIThread.", guidService));

                        packageAgent.SendGeneralError(exception);

                        throw exception;
                    }
                }
            }
            else
            {
                Debugger.Break();
            }

            packageAgent.SendGeneralMessage(string.Format("Service {{{0}}} does not implement interface {{{1}}}.", guidService, guidInterFace));

            return null;
        }

        public void ObjectDisposed(object sender, EventArgs e)
        {
            var guid = sender.GetType().GetGuid();

            if (sender is IHydraPackage)
            {
                this.LoadedPackages.Remove(guid);
            }
            else
            {
                this.LoadedServices.Remove(guid);
            }
        }

        public int GetProperty(HydraProperties propid, out object pvar)
        {
            throw new NotImplementedException();
        }

        public bool IsPackageInstalled(Guid guidPackage)
        {
            return PackageList.ContainsKey(guidPackage);
        }

        public bool IsPackageLoaded(Guid guidPackage, out IHydraPackage ppPackage)
        {
            if (LoadedPackages.ContainsKey(guidPackage))
            {
                ppPackage = LoadedPackages[guidPackage].Package;
                return true;
            }
            else
            {
                ppPackage = null;
                return false;
            }
        }

        public int SetProperty(HydraProperties propid, object var)
        {
            throw new NotImplementedException();
        }

        public bool ProfferService(Guid rguidService, IQueryServiceProvider psp, out uint pdwCookie)
        {
            if (!this.ServiceList.ContainsKey(rguidService))
            {
                this.ServiceList.Add(rguidService, new HydraServiceListItem { ServiceGuid = rguidService });
            }

            if (!this.LoadedServices.ContainsKey(rguidService))
            {
                this.LoadedServices.Add(rguidService, new HydraServiceWrapper(rguidService, psp));
            }

            pdwCookie = (uint)rguidService.ToString().GetHashCode();
            return false;
        }

        public bool RevokeService(uint dwCookie)
        {
            var guidService = this.LoadedServices.Where(kvp => kvp.Key.ToString().GetHashCode() == dwCookie).Select(kvp => kvp.Key).SingleOrDefault();

            if (guidService != Guid.Empty)
            {
                this.LoadedServices.Remove(guidService);
                return true;
            }
            else
            {
                return false;
            }
        }

        public IHydraStatusBar StatusBar
        {
            get 
            {
                return hydraUIInternals.StatusBar;
            }
        }

        public HydraDocumentContainer DocumentContainer
        {
            get 
            {
                return hydraUIInternals.DocumentContainer;
            }
        }

        public HydraDockSite DockSite
        {
            get 
            {
                return hydraUIInternals.DockSite;
            }
        }
    }
}
